SPB Git forge

spb/websensor

Public
33commits 1branches 0releases
3.4 MBsize
maindefault branch
10 days agolast push
TypeScript 55.4% Python 43.2% SQL 1.2%
28.4 KB · 481 lines tsx
Raw Blame History
1import Link from "next/link";2import type { Metadata } from "next";3import { notFound } from "next/navigation";4import { EventRow } from "@/components/event-row";5import { FieldChanges } from "@/components/field-changes";6import { LiveFeed } from "@/components/live-feed";7import { Badge, Bar, Chip, Empty, ExtLink, Flag, Heatmap, PageHeader, Panel, Score, Stat, Table, Tabs, Td, TierBadge, TypeChip } from "@/components/ui";8import { WatchButton } from "@/components/watch-button";9import { api, SITE_URL, type EntityDetail, type EntityInsights, type EventItem } from "@/lib/api";10import { agoIso, dayHeader, fmtInt, fmtScore, relTime, typeLabel, utcDate, utcDateTime, utcTime, withinLast } from "@/lib/format";1112export const dynamic = "force-dynamic";1314type Tab = "overview" | "live" | "sources" | "silent" | "timeline" | "related" | "metrics";15const TABS: Tab[] = ["overview", "live", "sources", "silent", "timeline", "related", "metrics"];16const RANGES: { key: string; label: string; ms: number | null }[] = [17  { key: "1h", label: "1 h", ms: 3600e3 },18  { key: "6h", label: "6 h", ms: 6 * 3600e3 },19  { key: "24h", label: "24 h", ms: 24 * 3600e3 },20  { key: "7d", label: "7 d", ms: 7 * 86400e3 },21  { key: "30d", label: "30 d", ms: 30 * 86400e3 },22  { key: "all", label: "All", ms: null },23];2425type Search = { tab?: string; range?: string; cursor?: string };2627export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {28  const { id } = await params;29  const d = await api.entity(id);30  if (!d) return { title: "Entity not found" };31  const e = d.entity;32  const desc = e.description ?? `Every meaningful change WebSensor detected for ${e.name}: announcements, releases, pricing, documentation, incidents and silent changes — with evidence.`;33  return { title: `${e.name} — ${typeLabel(e.type)}`, description: desc, alternates: { canonical: `/entity/${e.id}` }, openGraph: { title: `${e.name} · WebSensor`, description: desc, url: `${SITE_URL}/entity/${e.id}` } };34}3536const EMPTY_INSIGHTS: EntityInsights = { heatmap: [], baseline_per_day: 0, today: 0, events_24h: 0, events_prev_24h: 0, velocity_ratio: 0, anomaly: { score: 0, ratio: 0, pct: 0 }, silent_24h: 0, breaking_24h: 0, sources_24h: 0, most_active_sensors: [], rank: { rank: null, total: 0, score: null } };3738function mainCountry(d: EntityDetail): string | null {39  const counts = new Map<string, number>();40  for (const s of d.sources) if (s.country) counts.set(s.country, (counts.get(s.country) ?? 0) + (s.first_party ? 2 : 1));41  let best: string | null = null;42  let n = 0;43  for (const [c, k] of counts) if (k > n) [best, n] = [c, k];44  return best;45}4647export default async function EntityPage({ params, searchParams }: { params: Promise<{ id: string }>; searchParams: Promise<Search> }) {48  const { id } = await params;49  const sp = await searchParams;50  const d = await api.entity(id);51  if (!d) notFound();52  const e = d.entity;53  const tab: Tab = TABS.includes(sp.tab as Tab) ? (sp.tab as Tab) : "overview";54  const ins = d.insights ?? EMPTY_INSIGHTS;55  const silent = d.silent ?? [];56  const related = d.related ?? [];57  const country = mainCountry(d);58  const active = withinLast(e.last_event_at, 7 * 86400e3);59  const anomalous = ins.anomaly.score >= 60;60  const delta = ins.events_24h - ins.events_prev_24h;61  const href = (t: Tab): string => (t === "overview" ? `/entity/${e.id}` : `/entity/${e.id}?tab=${t}`);6263  return (64    <>65      <PageHeader66        compact67        kicker={68          <span className="flex flex-wrap items-center gap-x-2 gap-y-1">69            <TypeChip type={e.type} href={`/entities?tab=all&type=${e.type}`} />70            {d.parent && (71              <Link href={`/entity/${d.parent.id}`} className="inline-flex items-center gap-1 text-fg-muted hover:text-fg" title={`Parent: ${d.parent.name}`}>72                <span className="text-fg-subtle">↑</span> {d.parent.name}73              </Link>74            )}75            {e.domain && <Link href={`/domain/${e.domain}`} className="font-mono text-fg-muted hover:text-fg hover:underline">{e.domain}</Link>}76            {country && <Flag code={country} className="text-[12px]" />}77            <Chip tone={active ? "signal" : "default"} className="font-mono font-semibold tracking-wider" title={active ? "Events detected in the last 7 days" : "No event in the last 7 days"}>78              <span className={`inline-block size-1.5 rounded-full ${active ? "bg-signal" : "bg-low"}`} /> {active ? "ACTIVE" : "QUIET"}79            </Chip>80            {ins.rank.rank !== null && (81              <span className="font-mono text-fg-muted tabular" title={`WebSensor rank score ${fmtScore(ins.rank.score)} — importance × activity × velocity × quality × confirmation`}>82                <span className="text-fg-subtle">WebSensor rank</span> <Link href="/entities" className="font-semibold text-fg hover:underline">#{fmtInt(ins.rank.rank)}</Link> <span className="text-fg-subtle">of {fmtInt(ins.rank.total)}</span>83              </span>84            )}85          </span>86        }87        title={e.name}88        description={e.description}89        actions={90          <>91            <WatchButton kind="entity" value={e.id} />92            <Link href={`/alerts?entity=${encodeURIComponent(e.id)}`} className="rounded-md border border-line bg-panel px-2.5 py-1 text-[12px] hover:border-line-strong">Add to alert</Link>93            {(e.homepage || e.domain) && <ExtLink href={e.homepage ?? `https://${e.domain}`} className="text-[12px]">Website ↗</ExtLink>}94          </>95        }96      />9798      <div className="mb-3 grid grid-cols-2 gap-px overflow-hidden rounded-lg border border-line bg-line sm:grid-cols-3 xl:grid-cols-6">99        <div className="bg-panel">100          <Stat101            label="Activity 24 h"102            value={fmtInt(ins.events_24h)}103            hint={104              <span className={delta > 0 ? "text-signal" : delta < 0 ? "text-fg-subtle" : ""}>105                {delta > 0 ? "↑" : delta < 0 ? "↓" : "→"} {Math.abs(delta)} vs prev 24 h · ×{ins.velocity_ratio.toFixed(2)}106              </span>107            }108          />109        </div>110        <div className="bg-panel"><Stat label="Breaking 24 h" value={fmtInt(ins.breaking_24h)} tone={ins.breaking_24h > 0 ? "hot" : undefined} hint="signal ≥ 80" /></div>111        <div className="bg-panel"><Stat label="Silent 24 h" value={fmtInt(ins.silent_24h)} tone={ins.silent_24h > 0 ? "silent" : undefined} hint="no matching announcement" /></div>112        <div className="bg-panel"><Stat label="Sources 24 h" value={fmtInt(ins.sources_24h)} hint={`${fmtInt(d.sources.length)} linked`} /></div>113        <div className="bg-panel"><Stat label="30-day baseline" value={<>{ins.baseline_per_day.toFixed(1)}<span className="text-[11px] font-normal text-fg-subtle">/day</span></>} hint={`today ${fmtInt(ins.today)}`} /></div>114        <div className="bg-panel">115          <Stat116            label={anomalous ? "Anomalous activity" : "Anomaly"}117            value={ins.anomaly.score > 0 ? fmtScore(ins.anomaly.score) : "0"}118            tone={anomalous ? "hot" : ins.anomaly.score >= 40 ? "warn" : undefined}119            hint={<span className={anomalous ? "font-semibold text-hot" : ""}>{ins.anomaly.pct > 0 ? "+" : ""}{Math.round(ins.anomaly.pct)}% vs baseline</span>}120          />121        </div>122      </div>123124      <div className="panel mb-3 flex flex-wrap items-center gap-x-4 gap-y-2 px-3 py-2">125        <span className="label">35-day activity</span>126        <div className="min-w-0"><Heatmap days={ins.heatmap} /></div>127        <span className="ml-auto flex items-center gap-1 text-[10.5px] text-fg-subtle">128          less <span className="size-2.5 rounded-[2px] heat-1" /><span className="size-2.5 rounded-[2px] heat-2" /><span className="size-2.5 rounded-[2px] heat-3" /><span className="size-2.5 rounded-[2px] heat-4" /> more129          <span className="ml-2 size-2.5 rounded-[2px] heat-hot" /> ≥ 3 breaking130        </span>131      </div>132133      <Tabs134        className="mb-3"135        current={tab}136        items={[137          { key: "overview", label: "Overview", href: href("overview") },138          { key: "live", label: "Live", href: href("live") },139          { key: "sources", label: "Sources", href: href("sources"), count: d.sources.length },140          { key: "silent", label: <span className={silent.length ? "text-silent" : ""}>Silent</span>, href: href("silent"), count: silent.length },141          { key: "timeline", label: "Timeline", href: href("timeline") },142          { key: "related", label: "Related", href: href("related"), count: related.length + d.children.length },143          { key: "metrics", label: "Metrics", href: href("metrics") },144        ]}145      />146147      {tab === "overview" && <Overview d={d} ins={ins} />}148      {tab === "live" && <LiveFeed initial={d.recent} initialCursor={d.nextCursor ?? null} extraQuery={{ entity: e.id }} showTabs={false} title={`LIVE · ${e.name.toUpperCase()}`} />}149      {tab === "sources" && <SourcesTab d={d} />}150      {tab === "silent" && <SilentTab items={silent} name={e.name} />}151      {tab === "timeline" && <TimelineTab id={e.id} range={sp.range} cursor={sp.cursor} />}152      {tab === "related" && <RelatedTab d={d} />}153      {tab === "metrics" && <MetricsTab d={d} ins={ins} />}154    </>155  );156}157158// ---------------------------------------------------------------------------------------159160function Overview({ d, ins }: { d: EntityDetail; ins: EntityInsights }) {161  const e = d.entity;162  const related = d.related ?? [];163  return (164    <div className="grid gap-4 xl:grid-cols-[1fr_340px]">165      <Panel title={<>Recent signals <span className="font-mono text-fg-subtle">{d.recent.length}</span></>} dense action={<Link href={`/entity/${e.id}?tab=timeline`} className="text-[11px] text-fg-subtle hover:text-fg">full timeline →</Link>}>166        {d.recent.length ? d.recent.map((ev) => <EventRow key={ev.id} ev={ev} showDate />) : <Empty>No event detected yet for {e.name}. Sensors linked to this entity are checked continuously.</Empty>}167      </Panel>168      <aside className="flex min-w-0 flex-col gap-4">169        <SensorsPanel sensors={ins.most_active_sensors} />170        <Panel title="Related entities" dense>171          {related.length ? (172            <div className="flex flex-wrap gap-1 p-2">173              {related.slice(0, 16).map((r) => (174                <Chip key={r.id} href={`/entity/${r.id}`} title={`${typeLabel(r.type)} · ${r.shared_events} shared event${r.shared_events === 1 ? "" : "s"}`}>175                  {r.name} <span className="font-mono text-fg-subtle tabular">{r.shared_events}</span>176                </Chip>177              ))}178            </div>179          ) : (180            <Empty>No co-occurring entity yet.</Empty>181          )}182        </Panel>183        {d.children.length > 0 && (184          <Panel title={<>Products & children <span className="font-mono text-fg-subtle">{d.children.length}</span></>} dense>185            <ul className="divide-y divide-line">186              {d.children.slice(0, 12).map((c) => (187                <li key={c.id} className="flex items-center justify-between gap-2 px-3 py-1.5 text-[12.5px]">188                  <Link href={`/entity/${c.id}`} className="min-w-0 truncate font-medium hover:underline">{c.name}</Link>189                  <span className="shrink-0 font-mono text-[11px] text-fg-subtle tabular">{typeLabel(c.type)} · {fmtInt(c.event_count)}</span>190                </li>191              ))}192              {d.children.length > 12 && <li className="px-3 py-1.5 text-[11px] text-fg-subtle"><Link href={`/entity/${e.id}?tab=related`} className="hover:text-fg">+{d.children.length - 12} more →</Link></li>}193            </ul>194          </Panel>195        )}196        {d.relations.length > 0 && (197          <Panel title="Knowledge graph" dense>198            <ul className="divide-y divide-line">199              {d.relations.slice(0, 10).map((r, i) => (200                <li key={`${r.from_id}-${r.relation}-${r.to_id}-${i}`} className="flex flex-wrap items-center gap-x-1.5 px-3 py-1.5 text-[12.5px]">201                  <Link href={`/entity/${r.from_id}`} className={r.from_id === e.id ? "text-fg-muted" : "font-medium hover:underline"}>{r.from_name}</Link>202                  <span className="font-mono text-[10.5px] uppercase tracking-wider text-fg-subtle">{r.relation.replace(/_/g, " ")}</span>203                  <Link href={`/entity/${r.to_id}`} className={r.to_id === e.id ? "text-fg-muted" : "font-medium hover:underline"}>{r.to_name}</Link>204                  <Chip className="ml-auto">{typeLabel(r.to_type)}</Chip>205                </li>206              ))}207            </ul>208          </Panel>209        )}210        {d.aliases.length > 0 && (211          <Panel title="Aliases">212            <div className="flex flex-wrap gap-1">{d.aliases.map((a) => <Chip key={a} className="font-mono">{a}</Chip>)}</div>213          </Panel>214        )}215      </aside>216    </div>217  );218}219220function SensorsPanel({ sensors, title = "Most active sensors · 7 d" }: { sensors: EntityInsights["most_active_sensors"]; title?: string }) {221  return (222    <Panel title={title} dense>223      {sensors.length ? (224        <ul className="divide-y divide-line">225          {sensors.slice(0, 8).map((s) => (226            <li key={s.id} className="grid grid-cols-[1fr_auto_2.5rem] items-center gap-x-2 px-3 py-1.5 text-[12.5px] hover:bg-panel-2/60">227              <div className="min-w-0">228                <Link href={`/sensor/${s.id}`} className="block truncate font-medium hover:underline">{s.name}</Link>229                <div className="flex min-w-0 items-center gap-1.5 font-mono text-[10.5px] text-fg-subtle tabular">230                  <Link href={`/source/${s.source_id}`} className="truncate uppercase tracking-wide hover:text-fg">{s.source_id}</Link>231                  <span className="shrink-0">· {relTime(s.last_event_at)}</span>232                </div>233              </div>234              <Chip className="font-mono">{s.type}</Chip>235              <span className="text-right font-mono text-[12px] font-semibold text-signal tabular" title="Events in the last 7 days">{s.events_7d}</span>236            </li>237          ))}238        </ul>239      ) : (240        <Empty>No sensor produced an event for this entity in the last 7 days.</Empty>241      )}242    </Panel>243  );244}245246function SourcesTab({ d }: { d: EntityDetail }) {247  return (248    <Panel title={<>Monitored sources <span className="font-mono text-fg-subtle">{d.sources.length}</span></>} dense>249      {d.sources.length === 0 ? (250        <Empty>No monitored source is linked to {d.entity.name} yet. Events still resolve to it from third-party coverage.</Empty>251      ) : (252        <Table head={["Source", "Tier", "Evidence", "Country", "Sensors", "24 h", ""]}>253          {[...d.sources].sort((a, b) => (b.events_24h ?? 0) - (a.events_24h ?? 0) || Number(Boolean(b.first_party)) - Number(Boolean(a.first_party))).map((s) => (254            <tr key={s.id} className="hover:bg-panel-2/60">255              <Td>256                <Link href={`/source/${s.id}`} className="font-medium hover:underline">{s.name}</Link>257                <div className="truncate font-mono text-[11px] text-fg-subtle">{s.domain}</div>258              </Td>259              <Td><TierBadge tier={s.tier} /></Td>260              <Td>{s.first_party ? <Badge kind="first-party" /> : <Badge kind="external" />}</Td>261              <Td>{s.country ? <span className="inline-flex items-center gap-1.5"><Flag code={s.country} /><span className="font-mono text-[11px] text-fg-subtle">{s.country}</span></span> : <span className="text-fg-subtle">—</span>}</Td>262              <Td mono>{s.sensor_count ?? 0}</Td>263              <Td mono className={s.events_24h ? "text-signal" : "text-fg-subtle"}>{s.events_24h ?? 0}</Td>264              <Td className="text-right"><Link href={`/source/${s.id}`} className="whitespace-nowrap text-[11.5px] text-fg-subtle hover:text-fg">sensors →</Link></Td>265            </tr>266          ))}267        </Table>268      )}269    </Panel>270  );271}272273function SilentTab({ items, name }: { items: EventItem[]; name: string }) {274  return (275    <Panel title={<span className="inline-flex items-center gap-2"><Badge kind="silent" /> Silent changes <span className="font-mono text-fg-subtle">{items.length}</span></span>} dense action={<Link href={`/live?entity=${encodeURIComponent(name)}&silent_change=true`} className="hidden text-[11px] text-fg-subtle hover:text-fg sm:inline">all silent →</Link>}>276      {items.length === 0 ? (277        <Empty>No silent changes detected in this period.</Empty>278      ) : (279        <ul className="divide-y divide-line">280          {items.map((ev) => (281            <li key={ev.id} className="grid grid-cols-[auto_1fr] gap-x-3 border-l-2 border-l-silent/60 px-3 py-2 sm:grid-cols-[6.25rem_1fr_auto]">282              <div className="flex flex-col font-mono text-[11px] leading-4 text-fg-subtle tabular">283                <time dateTime={ev.detected_at} title={utcDateTime(ev.detected_at)} className="text-fg-muted">{utcTime(ev.detected_at)}</time>284                <span>{utcDate(ev.detected_at)}</span>285              </div>286              <div className="min-w-0 sm:col-start-2">287                <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[11px]">288                  <Link href={`/source/${ev.source?.id ?? ev.source_id}`} className="font-mono font-semibold uppercase tracking-wide text-fg-muted hover:text-fg">{ev.source?.name ?? ev.source_id}</Link>289                  {ev.country && <Flag code={ev.country} />}290                  <span className="sm:hidden"><Score value={ev.signal_score ?? ev.importance} size="sm" kind="signal" /></span>291                  <Chip>{typeLabel(ev.event_type)}</Chip>292                  {ev.change_class && <Chip tone="silent">{ev.change_class}</Chip>}293                </div>294                <Link href={`/event/${ev.slug}`} className="mt-0.5 block font-medium leading-snug hover:underline">{ev.title}</Link>295                {ev.field_changes && ev.field_changes.length > 0 ? (296                  <div className="mt-1.5 max-w-3xl"><FieldChanges items={ev.field_changes} compact max={4} /></div>297                ) : (298                  ev.summary && <p className="mt-0.5 line-clamp-2 text-[12.5px] text-fg-muted">{ev.summary}</p>299                )}300              </div>301              <div className="hidden items-start pt-0.5 sm:flex"><Score value={ev.signal_score ?? ev.importance} kind="signal" /></div>302            </li>303          ))}304        </ul>305      )}306    </Panel>307  );308}309310async function TimelineTab({ id, range, cursor }: { id: string; range?: string; cursor?: string }) {311  const r = RANGES.find((x) => x.key === range) ?? RANGES[2]!;312  const after = r.ms ? agoIso(r.ms) : undefined;313  const page = await api.events({ entity: id, after, limit: 100, cursor });314  const groups = new Map<string, EventItem[]>();315  for (const ev of page.items) {316    const day = utcDate(ev.detected_at);317    if (!groups.has(day)) groups.set(day, []);318    groups.get(day)!.push(ev);319  }320  const rangeHref = (k: string): string => `/entity/${id}?tab=timeline&range=${k}`;321  return (322    <Panel323      title={<>Timeline <span className="font-mono text-fg-subtle">{page.items.length}{page.nextCursor ? "+" : ""}</span></>}324      dense325      action={326        <nav className="flex items-center gap-0.5" aria-label="Range">327          {RANGES.map((x) => (328            <Link key={x.key} href={rangeHref(x.key)} aria-current={x.key === r.key ? "page" : undefined} className={`rounded-sm px-1.5 py-px font-mono text-[11px] tabular ${x.key === r.key ? "bg-panel-2 text-fg" : "text-fg-subtle hover:text-fg"}`}>{x.label}</Link>329          ))}330        </nav>331      }332    >333      {page.items.length === 0 ? (334        <Empty>No event in the last {r.label.replace(" ", "")}{r.ms ? "" : " — nothing recorded yet"}. Try a wider range.</Empty>335      ) : (336        <div>337          {[...groups.entries()].map(([day, evs]) => (338            <div key={day}>339              <div className="sticky top-12 z-10 flex items-center justify-between border-y border-line bg-panel-2/95 px-3 py-1 font-mono text-[11px] font-semibold tracking-wider text-fg-muted backdrop-blur">340                <span>{dayHeader(evs[0]!.detected_at)}</span>341                <span className="font-normal text-fg-subtle tabular">{evs.length}</span>342              </div>343              {evs.map((ev) => <EventRow key={ev.id} ev={ev} />)}344            </div>345          ))}346        </div>347      )}348      <div className="flex items-center justify-between px-3 py-2 text-[12px] text-fg-subtle">349        <span>{after ? <>since <span className="font-mono tabular">{utcDateTime(after)}</span></> : "complete history, newest first"}</span>350        {page.nextCursor && <Link href={`${rangeHref(r.key)}&cursor=${encodeURIComponent(page.nextCursor)}`} className="rounded-md border border-line bg-panel-2 px-2.5 py-1 text-fg hover:border-line-strong">Older →</Link>}351      </div>352    </Panel>353  );354}355356function RelatedTab({ d }: { d: EntityDetail }) {357  const related = d.related ?? [];358  const max = Math.max(1, ...related.map((r) => r.shared_events));359  return (360    <div className="grid gap-4 xl:grid-cols-[1fr_1fr]">361      <Panel title={<>Co-occurring entities <span className="font-mono text-fg-subtle">{related.length}</span></>} dense>362        {related.length === 0 ? (363          <Empty>No entity shares an event with {d.entity.name} yet.</Empty>364        ) : (365          <Table head={["Entity", "Type", "Shared events", ""]}>366            {related.map((r) => (367              <tr key={r.id} className="hover:bg-panel-2/60">368                <Td><Link href={`/entity/${r.id}`} className="font-medium hover:underline">{r.name}</Link></Td>369                <Td><Chip>{typeLabel(r.type)}</Chip></Td>370                <Td mono>{r.shared_events}</Td>371                <Td className="w-32 min-w-24 pt-3"><Bar value={r.shared_events} max={max} tone="info" /></Td>372              </tr>373            ))}374          </Table>375        )}376      </Panel>377      <div className="flex flex-col gap-4">378        <Panel title={<>Products & children <span className="font-mono text-fg-subtle">{d.children.length}</span></>} dense>379          {d.children.length === 0 ? (380            <Empty>No child entity.</Empty>381          ) : (382            <Table head={["Entity", "Type", "Events", "Last"]}>383              {d.children.map((c) => (384                <tr key={c.id} className="hover:bg-panel-2/60">385                  <Td><Link href={`/entity/${c.id}`} className="font-medium hover:underline">{c.name}</Link></Td>386                  <Td><Chip>{typeLabel(c.type)}</Chip></Td>387                  <Td mono>{fmtInt(c.event_count)}</Td>388                  <Td mono className="whitespace-nowrap text-fg-subtle">{c.last_event_at ? relTime(c.last_event_at) : "—"}</Td>389                </tr>390              ))}391            </Table>392          )}393        </Panel>394        <Panel title={<>Knowledge graph <span className="font-mono text-fg-subtle">{d.relations.length}</span></>} dense>395          {d.relations.length === 0 ? (396            <Empty>No relation recorded.</Empty>397          ) : (398            <ul className="divide-y divide-line">399              {d.relations.map((r, i) => (400                <li key={`${r.from_id}-${r.relation}-${r.to_id}-${i}`} className="flex flex-wrap items-center gap-x-1.5 px-3 py-1.5 text-[12.5px]">401                  <Link href={`/entity/${r.from_id}`} className={r.from_id === d.entity.id ? "text-fg-muted" : "font-medium hover:underline"}>{r.from_name}</Link>402                  <span className="font-mono text-[10.5px] uppercase tracking-wider text-fg-subtle">{r.relation.replace(/_/g, " ")}</span>403                  <Link href={`/entity/${r.to_id}`} className={r.to_id === d.entity.id ? "text-fg-muted" : "font-medium hover:underline"}>{r.to_name}</Link>404                  <Chip className="ml-auto">{typeLabel(r.to_type)}</Chip>405                </li>406              ))}407            </ul>408          )}409        </Panel>410        {d.aliases.length > 0 && (411          <Panel title="Aliases">412            <div className="flex flex-wrap gap-1">{d.aliases.map((a) => <Chip key={a} className="font-mono">{a}</Chip>)}</div>413          </Panel>414        )}415      </div>416    </div>417  );418}419420function MetricsTab({ d, ins }: { d: EntityDetail; ins: EntityInsights }) {421  const maxType = Math.max(1, ...d.by_type.map((t) => t.n));422  const total = d.by_type.reduce((n, t) => n + t.n, 0);423  const anomalous = ins.anomaly.score >= 60;424  const days7 = ins.heatmap.slice(-7).reduce((n, x) => n + x.events, 0);425  const days35 = ins.heatmap.reduce((n, x) => n + x.events, 0);426  const peak = ins.heatmap.reduce<{ day: string; events: number } | null>((best, x) => (!best || x.events > best.events ? { day: x.day, events: x.events } : best), null);427  return (428    <div className="grid gap-4 xl:grid-cols-[1fr_1fr]">429      <div className="flex flex-col gap-4">430        <Panel title={<>Event types <span className="font-mono text-fg-subtle">{fmtInt(total)}</span></>}>431          {d.by_type.length ? (432            <ul className="space-y-1.5">433              {d.by_type.map((t) => (434                <li key={t.event_type} className="grid grid-cols-[9rem_1fr_3.5rem] items-center gap-2 text-[12px] sm:grid-cols-[11rem_1fr_3.5rem]">435                  <Link href={`/live?entity=${encodeURIComponent(d.entity.id)}&event_type=${t.event_type}`} className="truncate hover:underline">{typeLabel(t.event_type)}</Link>436                  <Bar value={t.n} max={maxType} tone="info" />437                  <span className="text-right font-mono text-fg-subtle tabular">{t.n} <span className="text-[10.5px]">{total ? Math.round((t.n / total) * 100) : 0}%</span></span>438                </li>439              ))}440            </ul>441          ) : (442            <Empty>No events yet.</Empty>443          )}444        </Panel>445        <SensorsPanel sensors={ins.most_active_sensors} />446      </div>447      <div className="flex flex-col gap-4">448        <Panel title="Anomaly">449          <div className="flex items-baseline justify-between">450            <span className={`font-mono text-3xl font-semibold tabular ${anomalous ? "text-hot" : ins.anomaly.score >= 40 ? "text-warn" : "text-fg"}`}>{ins.anomaly.score > 0 ? fmtScore(ins.anomaly.score) : "0"}</span>451            <span className={`label ${anomalous ? "!text-hot" : ""}`}>{anomalous ? "ANOMALOUS ACTIVITY" : ins.anomaly.score >= 40 ? "ELEVATED" : "NORMAL"}</span>452          </div>453          <div className="mt-2"><Bar value={ins.anomaly.score} tone={anomalous ? "hot" : ins.anomaly.score >= 40 ? "high" : "signal"} /></div>454          <dl className="mt-3 grid grid-cols-2 gap-y-1 text-[12.5px]">455            <dt className="text-fg-subtle">30-day baseline</dt><dd className="text-right font-mono tabular">{ins.baseline_per_day.toFixed(1)} events/day</dd>456            <dt className="text-fg-subtle">Last 24 h</dt><dd className="text-right font-mono tabular">{fmtInt(ins.events_24h)} events</dd>457            <dt className="text-fg-subtle">Previous 24 h</dt><dd className="text-right font-mono tabular">{fmtInt(ins.events_prev_24h)} events</dd>458            <dt className="text-fg-subtle">Velocity ratio</dt><dd className="text-right font-mono tabular">×{ins.velocity_ratio.toFixed(2)}</dd>459            <dt className="text-fg-subtle">vs baseline</dt><dd className={`text-right font-mono tabular ${anomalous ? "font-semibold text-hot" : ""}`}>{ins.anomaly.pct > 0 ? "+" : ""}{Math.round(ins.anomaly.pct)}% (×{ins.anomaly.ratio.toFixed(2)})</dd>460          </dl>461          <p className="mt-3 text-[12px] leading-relaxed text-fg-muted">462            The anomaly score compares the last 24 h with this entity&apos;s own 30-day baseline, so a busy organization is not flagged for being busy. Scores ≥ 60 mark anomalous activity; the ratio above 1 means more events than usual.463          </p>464        </Panel>465        <Panel title="35-day activity">466          <Heatmap days={ins.heatmap} />467          <dl className="mt-3 grid grid-cols-3 gap-2 text-[12.5px]">468            <div><dt className="label">7 d</dt><dd className="font-mono text-base font-semibold tabular">{fmtInt(days7)}</dd></div>469            <div><dt className="label">35 d</dt><dd className="font-mono text-base font-semibold tabular">{fmtInt(days35)}</dd></div>470            <div className="min-w-0"><dt className="label">Peak day</dt><dd className="truncate font-mono text-base font-semibold tabular">{peak && peak.events ? <>{peak.events} <span className="text-[11px] font-normal text-fg-subtle">{peak.day}</span></> : "—"}</dd></div>471          </dl>472        </Panel>473        <div className="grid grid-cols-2 gap-px overflow-hidden rounded-lg border border-line bg-line">474          <div className="bg-panel"><Stat label="Total events" value={fmtInt(d.entity.event_count)} hint={d.entity.last_event_at ? `last ${relTime(d.entity.last_event_at)}` : "none yet"} /></div>475          <div className="bg-panel"><Stat label="Entity importance" value={fmtScore(d.entity.importance)} hint="weight in scoring" /></div>476        </div>477      </div>478    </div>479  );480}481